extern crate crypto;
extern crate strum;
use crate::protocol::{
    ERROR_PREFIX, INVALID_PASSWORD_MESSAGE, SERVER_ONLINE_HTML, SUCCESS_PREFIX,
    WELCOME_MESSAGE,
};
use crate::util::{extract_clip_meta, ClipMeta, VERSION};
use clap::Args;
use clipboard_rs::{ClipboardContext, ClipboardHandler, ClipboardWatcher, ClipboardWatcherContext};
use crypto::digest::Digest;
use std::{
    io::{BufRead, BufReader, Write},
    net::{TcpListener, TcpStream},
    process::exit,
    str::FromStr,
    sync::{Arc, Mutex},
    thread::{self, JoinHandle},
};
use strum::{Display, EnumString};

#[derive(Args, Clone)]
pub struct ServeArgs {
    /// listen port
    #[arg(short, long)]
    pub port: Option<i32>,
    /// watch clipboard changes
    #[arg(short, long)]
    pub watcher: bool,
    /// password
    #[arg(short, long)]
    pub password: Option<String>,
}
struct ServerContext {
    clipboard: ClipboardContext,
    hash: String,
    clip_meta: ClipMeta,
    password: String,
}

impl ServerContext {
    fn new(password: String) -> Self {
        Self {
            clipboard: ClipboardContext::new().unwrap(),
            hash: String::new(),
            clip_meta: ClipMeta::new(),
            password,
        }
    }
}

pub fn run(args: &ServeArgs) {
    // generate global random password if not provided
    let global_password = args
        .password
        .as_ref()
        .map(|s| s.to_string())
        .unwrap_or_else(|| {
            use rand::{thread_rng, Rng};
            let mut rng = thread_rng();
            let password: String = (0..12)
                .map(|_| rng.sample(rand::distributions::Alphanumeric) as char)
                .collect();
            password
        });

    // show using port
    let port = args.port.unwrap_or(5690);
    // start server and show join url
    let server_description = String::from(WELCOME_MESSAGE)
        .replace("{author}", "codimiracle")
        .replace("{version}", VERSION)
        .replace("{password}", &global_password)
        .to_string();
    println!("{}", server_description);
    let address = String::from("0.0.0.0:") + &port.to_string();
    let result = TcpListener::bind(&address);

    let context = ServerContext::new(global_password);
    let arc = Arc::new(Mutex::new(context));

    match result {
        Ok(listener) => {
            println!("server running at: {}\n", &address);
            let mut handles: Vec<JoinHandle<()>> = Vec::new();
            if args.watcher {
                println!("--- clipboard watcher");
                let watcher = thread::spawn({
                    let watcher_mutex = Arc::clone(&arc);
                    move || {
                        watcher_job(watcher_mutex);
                    }
                });
                handles.push(watcher);
            }

            let worker = thread::spawn({
                let worker_mutex = Arc::clone(&arc);
                move || {
                    if let Ok(mut locked) = worker_mutex.lock() {
                        worker_job(&mut *locked, listener);
                    }
                }
            });
            handles.push(worker);

            ctrlc::set_handler(|| {
                println!("clipflow terminated.");
                exit(0);
            })
            .expect("Error setting Ctrl-C handler");

            for ele in handles {
                ele.join();
            }
        }
        Err(error) => {
            println!("error: {:?}", error);
        }
    }
}

fn watcher_job(context: Arc<Mutex<ServerContext>>) {
    let handler = ClipboardServerBackgoundHandler::new(context);
    let mut watcher = ClipboardWatcherContext::new().unwrap();
    watcher.add_handler(handler);
    watcher.start_watch();
}

fn worker_job(context: &mut ServerContext, listener: TcpListener) {
    for stream in listener.incoming() {
        match stream {
            Ok(incom_stream) => {
                handle_connection(context, incom_stream);
            }
            Err(_error) => {
                // eprintln!("error during handle incoming connection: {}", error);
            }
        }
    }
}

fn handle_connection(ctx: &mut ServerContext, mut stream: TcpStream) {
    let buf_reader: BufReader<&mut TcpStream> = BufReader::new(&mut stream);
    let request: Vec<_> = buf_reader
        .lines()
        .map(|result| result.unwrap())
        .take_while(|line| !line.is_empty())
        .collect();

    if let Some(first_line) = request.first() {
        if first_line.contains(" HTTP/") {
            stream.write_all(SERVER_ONLINE_HTML.as_bytes()).unwrap();
        } else {
            let mut iter = request.iter();
            // remove instruction name line
            let _inst_line = iter.next();
            let password = iter.next();

            if password.is_none() || password.unwrap() != &ctx.password {
                println!(
                    "--- connection auth failed: {}: {}",
                    INVALID_PASSWORD_MESSAGE,
                    password.unwrap_or(&"".to_string())
                );
                stream
                    .write_all(body_failed(INVALID_PASSWORD_MESSAGE).as_bytes())
                    .unwrap();
                stream.flush().unwrap();
                return;
            }

            let args: Vec<_> = iter.collect();

            let inst = if let Ok(inst) = Instruction::from_str(&first_line) {
                inst
            } else {
                Instruction::None(first_line.to_string())
            };
            let result = handle_clipflow_command(ctx, inst, args);
            println!("--- connection reply: \n{}", result.as_str());
            stream.write_all(result.as_bytes());
            stream.flush();
        }
    }
}

#[derive(EnumString, Display, Debug, PartialEq)]
pub enum Instruction {
    ReadClips,
    WriteClips,
    ReadHash,
    None(String),
}

fn body(code: i8, message: &str, payload: &str) -> String {
    if payload.is_empty() {
        format!("{}) {}", code, message)
    } else {
        format!("{}) {}\n{}", code, message, payload)
    }
}

fn body_success(message: &str, data: Option<&String>) -> String {
    if data.is_none() {
        format!("{}{}", SUCCESS_PREFIX, message)
    } else {
        format!("{}{}\n{}", SUCCESS_PREFIX, message, data.unwrap())
    }
}
fn body_failed(message: &str) -> String {
    format!("{}{}", ERROR_PREFIX, message)
}

fn handle_clipflow_command(
    ctx: &mut ServerContext,
    instruction: Instruction,
    data: Vec<&String>,
) -> String {
    match instruction {
        Instruction::None(raw) => {
            return String::from("0) No instruction found: ") + &raw;
        }
        Instruction::ReadClips => {
            let json = serde_json::to_string(&ctx.clip_meta).unwrap();
            return body_success("ok", Some(&json));
        }
        Instruction::WriteClips => {
            let clip_meta_json = data[0];
            let clip_meta: ClipMeta = serde_json::from_str(clip_meta_json).unwrap();
            ctx.clip_meta = clip_meta;
            ctx.hash = hash_str(clip_meta_json);
            show_clipboard(&ctx);
            return body_success("ok", Some(&ctx.hash));
        }
        Instruction::ReadHash => body_success("ok", Some(&ctx.hash)),
    }
}
fn show_clipboard(context: &ServerContext) {
    let json = serde_json::to_string(&context.clip_meta).unwrap();
    println!("-- clips: {}", json);
    println!("-- hash: {}", context.hash);
}
fn hash_str(str: &str) -> String {
    let mut md5: crypto::md5::Md5 = crypto::md5::Md5::new();
    md5.input_str(str);
    let md5_str = md5.result_str();
    md5_str
}

pub struct ClipboardServerBackgoundHandler {
    server_context: Arc<Mutex<ServerContext>>,
}
impl ClipboardServerBackgoundHandler {
    pub fn new(context: Arc<Mutex<ServerContext>>) -> Self {
        Self {
            server_context: context,
        }
    }
}

impl ClipboardHandler for ClipboardServerBackgoundHandler {
    fn on_clipboard_change(&mut self) {
        let ctx = ClipboardContext::new().unwrap();
        let meta = extract_clip_meta(&ctx);
        let json = serde_json::to_string(&meta).unwrap();
        println!("clipboard changed: {}", json);
        let md5_str = hash_str(json.as_str());
        println!("clipboard hash: {}", md5_str.as_str());

        if let Ok(mut locked) = self.server_context.lock() {
            locked.hash.clear();
            locked.hash.push_str(md5_str.as_str());
            show_clipboard(&locked);
        }
    }
}
